package com.lsh.oms.mis.service.base;

import com.alibaba.dubbo.common.utils.StringUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.lsh.base.common.exception.BusinessException;
import com.lsh.oms.core.constant.EsConstants;
import com.lsh.oms.core.constant.LoggerConstant;
import com.lsh.oms.core.enums.EC;
import com.lsh.oms.core.enums.EsOrderType;
import com.lsh.oms.core.model.http.HttpMessage;
import com.lsh.oms.core.service.tool.ToolService;
import com.lsh.oms.core.utils.HttpUtils;
import com.lsh.oms.core.utils.LoggerUtils;
import com.lsh.oms.mis.model.EsMapping;
import com.lsh.oms.mis.model.Mapping;
import com.lsh.oms.mis.service.es.EsAsyncService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.annotation.PostConstruct;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.*;

/**
 * ElasticSearch Base Service
 * Created by panxudong on 16/12/06.
 */
@Component
public class EsService {

    private static Logger logger = LoggerFactory.getLogger(EsAsyncService.class);

    @Value("${elasticsearch.createStatus}")
    private String elasticsearchCreateStatus;

    @Value("${elasticsearch.updateStatus}")
    private String elasticsearchUpdateStatus;

    @Value("${elasticsearch.selectStatus}")
    private String elasticsearchSelectStatus;

    @Value("${elasticsearch.logName}")
    private String elasticsearchLogName;

    @Value("${elasticsearch.logBatchName}")
    private String elasticsearchLogBatchName;

    @Value("classpath:props/elasticsearch_mapping.xml")
    private Resource elasticsearchMapping;

    private Map<String, EsMapping> esMappingMap;

    private Map<Boolean, Set<String>> statusMap;

    private Set<String> selectStatusSet;

    private Logger esLogger;

    private Logger esBatchLogger;

    @PostConstruct
    public void init() throws Exception {
        esLogger = LoggerFactory.getLogger(elasticsearchLogName);

        esBatchLogger = LoggerFactory.getLogger(elasticsearchLogBatchName);

        statusMap = new HashMap<>();
        statusMap.put(true, ToolService.contentToSet(elasticsearchCreateStatus));
        statusMap.put(false, ToolService.contentToSet(elasticsearchUpdateStatus));

        esMappingMap = EsService.xmlToEsMapping(elasticsearchMapping.getFile());

        selectStatusSet = ToolService.contentToSet(elasticsearchSelectStatus);
    }

    public <T> Map<String, Map<String, Object>> batchSaveOrUpdate(List<T> orderList, boolean isCreate, String uuid, String operateType) throws Exception {
        String message = " [BatchSaveOrUpdate index][" + uuid + "]";
        esBatchLogger.info(message + "[Params:" + JSON.toJSONString(orderList) + "]");

        if(orderList.isEmpty()) {
            return null;
        }

        EsMapping esMapping = this.getEsMapping(orderList.get(0), message);

        String batchPrefix = isCreate ? "index" : "update";
        StringBuffer sbf = new StringBuffer();
        Map<String, String> requestMap = new HashMap<>();
        for(T order : orderList) {
            String code = this.getKey(order, esMapping, message);

            String requestIndex = "{ \"" + batchPrefix + "\": {\"_id\": \"" + code + "\"}}";
            sbf.append(requestIndex).append(System.getProperty("line.separator"));

            String requestData = EsService.getRequestData(order, esMapping, uuid, isCreate);
            sbf.append(requestData).append(System.getProperty("line.separator"));

            requestMap.put(code, requestData);
        }

        String suffix = "_bulk";
        String url = esMapping.getPath() + suffix;
        String data = sbf.toString();
        HttpMessage httpMessage = null;
        try {
            LoggerUtils.saveLog(message + "[Send post request start]", LoggerConstant.LOGGER_INFO);
            esBatchLogger.info(message + "[Url:" + url + ", RequestData:" + data + "]");

            httpMessage = HttpUtils.sendPostRequest(url, data, HttpUtils.getDefaultHeadersWithContentType());

            LoggerUtils.saveLog(message + "[" + operateType
                            + " index info][Info status : " + httpMessage.getStatus() + "]",
                    LoggerConstant.LOGGER_INFO);
            esBatchLogger.info(message + "[Info status : " + httpMessage.getStatus() +
                    "][Info message:" + httpMessage.getMessage() + "]");
        } catch (IOException e) {
            LoggerUtils.saveLog(message + "[" + operateType
                            + " index error][Error class : " + e.getClass() + ", Error message : " + e.getMessage() + "]",
                    LoggerConstant.LOGGER_ERROR);
            logger.error(e.getMessage(), e);
            throw e;
        }

        String urlPrefix = esMapping.getPath();
        Map<String, Map<String, Object>> errorMap = EsService.analysisBulkLog(httpMessage.getMessage(), isCreate, uuid,
                batchPrefix, statusMap, requestMap, urlPrefix, EsService.getOrderType(esMapping));

        return errorMap;
    }

    public <T> Map<String, Object> saveOrUpdate(T order, String uuid, String operateType) throws Exception {
        String message = " [SaveOrUpdate index][" + uuid + "]";
        EsMapping esMapping = this.getEsMapping(order, message);

        String code = this.getKey(order, esMapping, message);

        boolean isCreate = true;
        if(this.isExist(code, uuid, esMapping)) {
            isCreate = false;
        }

        return this.executeSaveOrUpdate(order, esMapping, code, isCreate, uuid, message, operateType);
    }

    public <T> Map<String, Object> saveOrUpdate(T order, boolean isCreate, String uuid, String operateType) throws Exception {
        String message = " [SaveOrUpdate index][" + uuid + "]";
        EsMapping esMapping = this.getEsMapping(order, message);

        String code = this.getKey(order, esMapping, message);

        return this.executeSaveOrUpdate(order, esMapping, code, isCreate, uuid, message, operateType);
    }

    public <T> Map<String, Object> deleteIndex(T order, String uuid, String operateType) throws Exception {
        String message = " [Delete index][" + uuid + "]";
        EsMapping esMapping = this.getEsMapping(order, message);
        String code = this.getKey(order, esMapping, message);
        String url = esMapping.getPath() + code;
        boolean isSuccess = true;

        for(int i=0;i<3;i++) {
            HttpMessage httpMessage = HttpUtils.sendDeleteRequest(url);

            if (!selectStatusSet.contains(httpMessage.getStatus())) {
                isSuccess = false;
            }

            LoggerUtils.saveLog(message + "[" + operateType
                            + " index info][Info status : " + httpMessage.getStatus() + ", Info message : " + httpMessage.getMessage() + "]",
                    LoggerConstant.LOGGER_INFO);

            if (isSuccess) {
                break;
            }
        }

        Map<String, Object> errorMap = null;
        if(!isSuccess) {
            errorMap = new HashMap<>();
            errorMap.put(EsConstants.ATTR_ERROR, EC.SEND_REQUEST_ERROR.getMessage());
            errorMap.put(EsConstants.ATTR_URL, url);
            errorMap.put(EsConstants.ATTR_KEY, code);
            errorMap.put("operateType", "delete");
            errorMap.put(EsConstants.ATTR_CODE, !StringUtils.isBlank(code) ? Long.parseLong(code) : null);
            errorMap.put(EsConstants.ATTR_ORDER_TYPE, EsService.getOrderType(esMapping));
        }
        return errorMap;
    }

    public String selectIndex(String searchUrl, String query, Boolean isCount) {
        String uuid = UUID.randomUUID().toString();
        LoggerUtils.saveLog(" [Select index][" + uuid + "][Url:" + searchUrl +
                "][Params : " + query + "]", LoggerConstant.LOGGER_INFO);

        try {
            HttpMessage httpMessage = HttpUtils.sendPostRequest(searchUrl, query, HttpUtils.getDefaultHeadersWithContentType());

            if(!selectStatusSet.contains(httpMessage.getStatus())) {
                LoggerUtils.saveLog(" [Select index][" + uuid + "][Select index error][Error status : " + httpMessage.getStatus()
                                + ", Error message : " + httpMessage.getMessage() + "]",
                        LoggerConstant.LOGGER_ERROR);
                return null;
            }

            LoggerUtils.saveLog(" [Select index][" + uuid + "][Select index info][Info status : " + httpMessage.getStatus() + "]",
                    LoggerConstant.LOGGER_INFO);
            esBatchLogger.info(" [Select index][" + uuid + "][Info status : " + httpMessage.getStatus() +
                    "][Info message:" + httpMessage.getMessage() + "]");
            return isCount ? ToolService.getEsTotal(httpMessage.getMessage()) : ToolService.getEsHits(httpMessage.getMessage());
        } catch (IOException e) {
            LoggerUtils.saveLog(" [Select index][" + uuid + "][Select index error][Error class : " + e.getClass() + ", Error message : " + e.getMessage() + "]",
                    LoggerConstant.LOGGER_ERROR);
            logger.error(e.getMessage(), e);
        }

        return null;
    }

    private <T> EsMapping getEsMapping(T order, String message) throws BusinessException {
        EsMapping esMapping = esMappingMap.get(order.getClass().getName().toString());
        if(esMapping == null) {
            LoggerUtils.saveLog(message + "[EsMappingMap do not have order class]" +
                            "[Order class:" + order.getClass().toString() + ", EsMappingMap:" + JSON.toJSONString(esMappingMap) + "]",
                    LoggerConstant.LOGGER_ERROR);
            throw EC.ERROR.exception("EsMappingMap do not have class!");
        }

        return esMapping;
    }

    private <T> String getKey(T order, EsMapping esMapping, String message) throws Exception {
        String code;
        try {
            code = EsService.getKey(order, esMapping);
        } catch (Exception e) {
            LoggerUtils.saveLog(message + "[Error class:" + e.getClass() +
                            ",EsMappingMap:" + JSON.toJSONString(esMappingMap) +
                            ",Error message:" + e.getMessage() + "]",
                    LoggerConstant.LOGGER_ERROR);
            logger.error(e.getMessage(), e);
            throw e;
        }
        return code;
    }

    private <T> Map<String, Object> executeSaveOrUpdate(T order, EsMapping esMapping, String code, boolean isCreate,
                                                        String uuid, String message, String operateType) throws IOException {
        String suffix = isCreate ? "/_create" : "/_update";
        String url = esMapping.getPath() + code + suffix;
        String requestData = EsService.getRequestData(order, esMapping, uuid, isCreate);
        boolean isSuccess = true;

        LoggerUtils.saveLog(message +
                "[Url : " + url + ", RequestData : " + requestData + "]", LoggerConstant.LOGGER_INFO);
        for(int i=0;i<3;i++) {
            try {
                isSuccess = true;

                HttpMessage httpMessage = HttpUtils.sendPostRequest(url, requestData, HttpUtils.getDefaultHeadersWithContentType());

                if(!statusMap.get(isCreate).contains(httpMessage.getStatus())) {
                    isSuccess = false;
                }

                LoggerUtils.saveLog(message + "[" + operateType
                                + " index info][Info status : " + httpMessage.getStatus() + ", Info message : " + httpMessage.getMessage() + "]",
                        LoggerConstant.LOGGER_INFO);

                if(isSuccess) {
                    break;
                }
            } catch (IOException e) {
                isSuccess = false;
                LoggerUtils.saveLog(message + "[" + operateType
                                + " index error][Error class : " + e.getClass() + ", Error message : " + e.getMessage() + "]",
                        LoggerConstant.LOGGER_ERROR);
                logger.error(e.getMessage(), e);
            }
        }

        Map<String, Object> errorMap = null;
        if(!isSuccess) {
            errorMap = new HashMap<>();
            errorMap.put(EsConstants.ATTR_ERROR, EC.SEND_REQUEST_ERROR.exception());
            errorMap.put(EsConstants.ATTR_URL, url);
            errorMap.put(EsConstants.ATTR_DATA, requestData);
            errorMap.put(EsConstants.ATTR_KEY, code);
            errorMap.put(EsConstants.ATTR_CODE, !StringUtils.isBlank(code) ? Long.parseLong(code) : null);
            errorMap.put(EsConstants.ATTR_ORDER_TYPE, EsService.getOrderType(esMapping));
        }
        return errorMap;
    }

    private static Integer getOrderType(EsMapping esMapping) {
        EsOrderType esOrderType = EsOrderType.getOrderType(esMapping.getName());
        Integer orderType = null;
        if(esOrderType != null) {
            orderType = esOrderType.getType();
        }

        return orderType;
    }

    private static <T> String getKey(T order, EsMapping esMapping) throws Exception {
        String key = esMapping.getKey();
        if(StringUtils.isBlank(key)) {
            throw new Exception("EsMapping do not have key");
        }

        List<Mapping> mappings = esMapping.getMappings();

        for(Mapping mapping : mappings) {
            if(StringUtils.isEquals(key, mapping.getField())) {
                try {
                    Object value = EsService.getFieldValue(order, mapping);
                    if(value == null || StringUtils.isBlank(String.valueOf(value))) {
                        throw new Exception("Key is null");
                    }
                    return String.valueOf(value);
                } catch (Exception e) {
                    throw e;
                }
            }
        }

        throw new Exception("Mappings do not have key field");
    }

    public boolean isExist(String code, String uuid, EsMapping esMapping) throws Exception {
        String searchUrl = esMapping.getPath() + code;

        boolean isExist = false;
        try {
            LoggerUtils.saveLog(" [IsExist index][" + uuid + "][Url:" + searchUrl +
                    "]", LoggerConstant.LOGGER_INFO);

            HttpMessage httpMessage = HttpUtils.sendGetRequest(searchUrl);

            LoggerUtils.saveLog(" [IsExist index][" + uuid + "][IsExist index info][Info status : " + httpMessage.getStatus()
                    + "][Info message:" + httpMessage.getMessage() + "]",
                    LoggerConstant.LOGGER_INFO);

            if(!StringUtils.isBlank(httpMessage.getMessage())) {
                JSONObject res = JSONObject.parseObject(httpMessage.getMessage());
                isExist = res.getBooleanValue(EsConstants.ATTR_FOUND);
            }
        } catch (IOException e) {
            LoggerUtils.saveLog(" [IsExist index][" + uuid + "][IsExist index error][Error class : " + e.getClass() + ", Error message : " + e.getMessage() + "]",
                    LoggerConstant.LOGGER_ERROR);
            logger.error(e.getMessage(), e);
        }
        return isExist;
    }

    private static <T> String getRequestData(T order, EsMapping esMapping, String uuid, boolean isCreate) {
        Map<String, Object> dataMap = EsService.getDataMap(order, esMapping, uuid, isCreate);
        String requestData;
        if(!isCreate) {
            Map<String, Object> requestMap = new HashMap<>();
            requestMap.put("doc", dataMap);
            requestData = JSON.toJSONString(requestMap, SerializerFeature.WriteMapNullValue,
                    SerializerFeature.WriteNullStringAsEmpty,
                    SerializerFeature.WriteNullNumberAsZero);
        } else {
            requestData = JSON.toJSONString(dataMap, SerializerFeature.WriteMapNullValue,
                    SerializerFeature.WriteNullStringAsEmpty,
                    SerializerFeature.WriteNullNumberAsZero);
        }
        return requestData;
    }

    private static <T> Map<String, Object> getDataMap(T order, EsMapping esMapping, String uuid, boolean isCreate) {
        Map<String, Object> dataMap = new HashMap<>();

        List<Mapping> mappings = esMapping.getMappings();
        for(Mapping mapping : mappings) {
            Object value = null;
            if(mapping.getValid()) {
                try {
                    value = EsService.getFieldValue(order, mapping);
                } catch (Exception e) {
                    LoggerUtils.saveLog(" [SaveOrUpdate index][GetFieldValue error][" + uuid +
                                    "][Mapping : " + mapping.getField() + ", Error class : " + e.getClass() + ", Error message : " + e.getMessage() + "]"
                            , LoggerConstant.LOGGER_ERROR);
                    logger.error(e.getMessage(), e);
                }
            }

            if(value == null || StringUtils.isBlank(String.valueOf(value))) {
                if(isCreate) {
                    if(mapping.getValue() != null && !StringUtils.isBlank(mapping.getValue())) {
                        value = EsService.getValue(mapping.getType(), mapping.getValue());
                    }
                } else {
                    continue;
                }
            }

            dataMap.put(mapping.getColumn(), value);
        }

        return dataMap;
    }

    private static <T> Object getFieldValue(T order, Mapping mapping) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
        String field = mapping.getField();
        try {
            String methodName = field.substring(0, 1).toUpperCase() + field.substring(1);
            Method method = order.getClass().getMethod("get" + methodName);

            Object value = method.invoke(order);

            if(value == null) {
                return value;
            }
            return EsService.getValue(mapping.getType(), value);
        } catch (NoSuchMethodException e) {
            throw e;
        } catch (InvocationTargetException e) {
            throw e;
        } catch (IllegalAccessException e) {
            throw e;
        }
    }

    private static Object getValue(String className, Object value) {
        if(StringUtils.isEquals(className, java.lang.Long.class.getName().toString())) {
            return Long.parseLong(String.valueOf(value));
        } else if (StringUtils.isEquals(className, java.lang.Integer.class.getName().toString())) {
            return Integer.parseInt(String.valueOf(value));
        } else if (StringUtils.isEquals(className, java.lang.Byte.class.getName().toString())) {
            return Byte.parseByte(String.valueOf(value));
        } else if (StringUtils.isEquals(className, java.math.BigDecimal.class.getName().toString())) {
            return new BigDecimal(String.valueOf(value));
        } else {
            return String.valueOf(value);
        }
    }

    private static Map<String, EsMapping> xmlToEsMapping(File file) throws Exception {
        Map<String, EsMapping> esMappingMap = new HashMap<>();

        try {
            DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            Document document = builder.parse(file);
            Element root = document.getDocumentElement();
            NodeList nodeList = root.getChildNodes();

            for(int i=0; i<nodeList.getLength(); i++) {
                org.w3c.dom.Node xmlNode = nodeList.item(i);
                if(xmlNode.getNodeType() == Node.ELEMENT_NODE) {
                    Element element = (Element)xmlNode;
                    NodeList nodeSonList = element.getChildNodes();
                    List<Mapping> mappings = new ArrayList<>();
                    for(int j=0; j<nodeSonList.getLength(); j++) {
                        org.w3c.dom.Node xmlSonNode = nodeSonList.item(j);
                        if(xmlSonNode.getNodeType() == Node.ELEMENT_NODE) {
                            Element sonElement = (Element) xmlSonNode;

                            Mapping mapping = new Mapping();
                            mapping.setField(sonElement.getAttribute(EsConstants.ATTR_FIELD));
                            mapping.setColumn(sonElement.getAttribute(EsConstants.ATTR_COLUMN));
                            mapping.setType(sonElement.getAttribute(EsConstants.ATTR_TYPE));
                            mapping.setValue(sonElement.getAttribute(EsConstants.ATTR_VALUE));

                            String valid = sonElement.getAttribute(EsConstants.ATTR_VALID);
                            Boolean isValid = true;
                            if(!StringUtils.isBlank(valid) && StringUtils.isEquals("false", valid)) {
                                isValid = false;
                            }
                            mapping.setValid(isValid);

                            mappings.add(mapping);
                        }
                    }

                    EsMapping esMapping = new EsMapping();
                    esMapping.setName(element.getAttribute(EsConstants.ATTR_NAME));
                    esMapping.setPath(element.getAttribute(EsConstants.ATTR_PATH));
                    esMapping.setKey(element.getAttribute(EsConstants.ATTR_KEY));
                    esMapping.setMappings(mappings);

                    esMappingMap.put(esMapping.getName(), esMapping);
                }
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            throw e;
        }

        return esMappingMap;
    }

    private static Map<String, Map<String, Object>> analysisBulkLog(String content, boolean isCreate, String uuid,
                                                                    String batchPrefix, Map<Boolean, Set<String>> statusMap,
                                                                    Map<String, String> requestMap, String pathPrefix,
                                                                    Integer orderType) {
        Map<String, Map<String, Object>> errorMap = new HashMap<>();

        JSONObject response = JSONObject.parseObject(content);
        if (!response.getBoolean(EsConstants.ATTR_ERRORS)) {
            return errorMap;
        }

        JSONArray items = response.getJSONArray(EsConstants.ATTR_ITEMS);
        Iterator<Object> iterator = items.iterator();
        String suffix = isCreate ? "/_create" : "/_update";
        while (iterator.hasNext()) {
            Object object = iterator.next();
            JSONObject item = (JSONObject) object;

            JSONObject detail = item.getJSONObject(batchPrefix);
            if (detail == null) {
                LoggerUtils.saveLog(" [BatchSaveOrUpdate index][" + uuid + "][" + batchPrefix + " log not exist][Item:"
                        + JSON.toJSONString(item) + "]", LoggerConstant.LOGGER_ERROR);
                continue;
            }

            String status = detail.getString(EsConstants.ATTR_STATUS);
            Boolean isSuccess = true;
            if (!statusMap.get(isCreate).contains(status)) {
                isSuccess = false;
            }

            if (!isSuccess) {
                String code = detail.getString(EsConstants.ATTR_ID);
                String errorMsg = detail.getString(EsConstants.ATTR_ERROR);

                LoggerUtils.saveLog(" [BatchSaveOrUpdate index][" + uuid + "][" + code + "][Error message:" + errorMsg + "]",
                        LoggerConstant.LOGGER_ERROR);

                String url = pathPrefix + code + suffix;
                String requestData = requestMap.get(code);

                Map<String, Object> map = new HashMap<>();
                map.put(EsConstants.ATTR_URL, url);
                map.put(EsConstants.ATTR_DATA, requestData);
                map.put(EsConstants.ATTR_ERROR, errorMsg);
                map.put(EsConstants.ATTR_CODE, !StringUtils.isBlank(code) ? Long.parseLong(code) : null);
                map.put(EsConstants.ATTR_ORDER_TYPE, orderType);

                errorMap.put(code, map);
            }
        }

        return errorMap;
    }

    private static String getQueryString(String key, String code) {
        String queryString = "{\"query\":{\"term\":{\"" + key + "\":\"" + code + "\"}}," +
                "\"_source\":\"" + key + "\", \"from\":0,\"size\":1}";
        return queryString;
    }
}
